Skip to content

feat: add apm info, apm outdated, and list_remote_refs#613

Open
sergio-sisternes-epam wants to merge 9 commits intomicrosoft:mainfrom
sergio-sisternes-epam:feature/info-versions-outdated
Open

feat: add apm info, apm outdated, and list_remote_refs#613
sergio-sisternes-epam wants to merge 9 commits intomicrosoft:mainfrom
sergio-sisternes-epam:feature/info-versions-outdated

Conversation

@sergio-sisternes-epam
Copy link
Copy Markdown
Collaborator

Description

Adds two new top-level commands and the underlying git ls-remote infrastructure to support package version inspection and staleness detection.

apm info PACKAGE [FIELD] — Show installed package metadata (name, version, description, ref, commit from lockfile). Pass versions as the field selector to list remote tags and branches without cloning.

apm outdated — Compare locked dependencies against remote refs. Tag-pinned deps use semver comparison; branch-pinned deps compare the locked commit SHA against the remote branch tip. Includes Rich progress feedback and configurable parallel checks (-j N, default 4).

list_remote_refs() — New function in github_downloader.py that wraps git ls-remote --tags --heads for all git hosts (GitHub, ADO, GHE) using a single unified code path with no host-specific APIs.

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Maintenance / refactor

Testing

  • Tested locally
  • All existing tests pass (3683 total)
  • Added tests for new functionality:
    • test_list_remote_refs.py — 29 tests (parsing, sorting, auth, error handling)
    • test_info_command.py — 15 tests (metadata display, versions, --global, lockfile ref)
    • test_outdated_command.py — 24 tests (tag/branch/SHA comparison, parallel, progress, edge cases)
    • test_deps_list_tree_info.py — 1 backward-compat test for apm deps info alias

Copilot AI review requested due to automatic review settings April 7, 2026 17:28
Sergio Sisternes and others added 7 commits April 7, 2026 19:29
Promote `apm deps info` to top-level `apm info <package> [field]`.
Add `versions` field selector to list remote tags/refs via git ls-remote.
Add `apm outdated` to compare locked deps against latest available tags.

- list_remote_refs() on GitHubPackageDownloader enumerates refs without cloning
- RemoteRef dataclass in models/dependency/types.py
- apm deps info kept as backward-compatible alias
- apm outdated supports --global and --verbose flags
- 83 new tests (3608 total, 0 failures)
- Updated CLI reference docs, dependency guide, commands skill, changelog

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove _list_refs_via_ado_api() and the ADO-specific branch in
list_remote_refs(). All hosts (GitHub, ADO, GitLab, generic) now use
the single git ls-remote code path. git ls-remote works against any
git remote including Azure DevOps when given an authenticated URL.

-69 lines removed, simpler to maintain.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Instead of marking branch-pinned dependencies as 'unknown', compare
the locked commit SHA against the remote branch tip SHA via
git ls-remote. This turns every branch-pinned dep into a meaningful
'up-to-date' or 'outdated' status.

- Add _find_remote_tip() helper to resolve branch/default-branch SHA
- Branch-pinned deps: compare locked SHA vs named branch tip
- No-ref deps: compare against main/master (default branch fallback)
- Tag-pinned deps: unchanged (semver comparison still used)
- Commit-pinned deps with unrecognized ref: still 'unknown'

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Better description: 'Show information about a package' (not just installed)
- Add --global/-g flag to inspect packages from user scope (~/.apm/)
- Add examples showing both local metadata and remote versions usage
- Document available fields (versions) in help text

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add --global flag to apm info documentation
- Update outdated behavior: SHA comparison for branch-pinned deps
- Update CHANGELOG with all new features under [Unreleased]
- Update skills commands.md summary

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Show Ref and Commit fields in apm info output when lockfile data
is available. Uses substring matching to find the lockfile entry
for the queried package (handles virtual packages and org/repo keys).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Extract per-dep check logic into _check_one_dep() for thread safety
- Add --parallel-checks / -j option (default: 4, 0 = sequential)
- Rich progress bar with spinner during remote ref checks
- ThreadPoolExecutor for concurrent git ls-remote calls
- Plain text fallback when Rich is unavailable
- 4 new tests covering parallel, sequential, custom workers, and error handling

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds new CLI capabilities for inspecting installed package metadata and checking whether locked dependencies are stale, backed by a unified git ls-remote implementation for enumerating remote tags/branches.

Changes:

  • Introduces top-level apm info PACKAGE [FIELD] (including FIELD=versions for remote refs) and apm outdated (remote staleness checks with optional parallelism).
  • Adds GitHubPackageDownloader.list_remote_refs() using git ls-remote --tags --heads, plus a RemoteRef model for parsed refs.
  • Updates docs/changelog and adds unit tests for ref enumeration, info, and outdated behavior.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/unit/test_outdated_command.py New unit tests for apm outdated scenarios (tags/branches/errors/parallelism).
tests/unit/test_list_remote_refs.py New unit tests for list_remote_refs() parsing/sorting/auth/error handling.
tests/unit/test_info_command.py New unit tests for apm info local metadata + versions field behavior.
tests/unit/test_deps_list_tree_info.py Adds back-compat assertion that apm deps info matches apm info.
src/apm_cli/models/dependency/types.py Adds RemoteRef dataclass for remote tags/branches.
src/apm_cli/models/dependency/init.py Re-exports RemoteRef.
src/apm_cli/models/apm_package.py Re-exports RemoteRef for backward-compatible import paths.
src/apm_cli/deps/github_downloader.py Implements list_remote_refs() + parsing/sorting helpers using git ls-remote.
src/apm_cli/commands/outdated.py New apm outdated command with Rich progress + parallel checks.
src/apm_cli/commands/info.py New top-level apm info command (local metadata + remote versions).
src/apm_cli/commands/deps/cli.py Refactors apm deps info to delegate to shared apm info helpers.
src/apm_cli/cli.py Registers new top-level commands (info, outdated).
packages/apm-guide/.apm/skills/apm-usage/commands.md Updates command matrix for new commands/alias behavior.
docs/src/content/docs/reference/cli-commands.md Documents apm info and apm outdated; updates deps info as alias.
docs/src/content/docs/guides/dependencies.md Updates guide examples to prefer apm info over apm deps info.
CHANGELOG.md Adds changelog entries for new commands/ref infrastructure.
Comments suppressed due to low confidence (1)

CHANGELOG.md:46

  • The new changelog bullets under [Unreleased] don't follow the existing entry style in this file (most entries end with a PR reference like (#562)). Also, the same features are duplicated under the released [0.8.10] section, which risks misrepresenting what actually shipped in that release. Please format the new bullets with PR numbers and keep them only under [Unreleased] until they are released.
### Added

- Artifactory archive entry download for virtual file packages (#525)

### Added

- `apm info <package> [field]` command for inspecting package metadata and remote refs
- `apm info <package> versions` field selector lists remote tags and branches via `git ls-remote`
- `apm outdated` command compares locked dependencies against remote refs
- `--parallel-checks` (`-j`) option on `apm outdated` for concurrent remote checks (default: 4)
- Rich progress feedback during `apm outdated` dependency checking
- `--global` flag on `apm info` for inspecting user-scope packages

### Changed

- Scope resolution now happens once via `TargetProfile.for_scope()` and `resolve_targets()` -- integrators no longer need scope-aware parameters (#562)
- Unified integration dispatch table in `dispatch.py` -- both install and uninstall import from one source of truth (#562)
- Hook merge logic deduplicated: three copy-pasted JSON-merge methods replaced with `_integrate_merged_hooks()` + config dict (#562)
- `apm outdated` uses SHA comparison for branch-pinned deps instead of reporting them as `unknown`

### Fixed

- Reject symlinked primitive files in all discovery and resolution paths to prevent symlink-based traversal attacks (#596)
- `apm install -g` now deploys hooks to the scope-resolved target directory instead of hardcoding `.github/hooks/` (#565, #566)
- Hook sync/cleanup derives prefixes dynamically from `KNOWN_TARGETS` instead of hardcoded paths (#565)
- `auto_create=False` targets no longer get directories unconditionally created during install (#576)
- `apm deps update -g` now correctly passes scope, preventing user-scope updates from silently using project-scope paths (#562)
- Subprocess encoding failures on Windows non-UTF-8 consoles (CP950/CP936) -- all subprocess calls now use explicit UTF-8 encoding (#591)
- PowerShell 5.1 compatibility: replace multi-argument `Join-Path` calls with nested two-argument calls (#593)
- `apm marketplace add` now respects `GITHUB_HOST` environment variable for GitHub Enterprise users (#589)
- `compilation.exclude` patterns now filter primitive discovery, preventing excluded files from leaking into compiled output (#477)
- Runtime detection in script runner now uses anchored patterns to prevent false positives when runtime keywords appear in flag values (#563)
- `apm compile` now warns when instructions are missing `applyTo` across all compilation modes (#449)
- Detect remote default branch instead of hardcoding `main` (#574)
- Warn when two packages deploy a native skill with the same name (#545)

Comment on lines +49 to +55
direct_match = apm_modules_path / package
if direct_match.is_dir() and (
(direct_match / APM_YML_FILENAME).exists()
or (direct_match / SKILL_MD_FILENAME).exists()
):
return direct_match

Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolve_package_path() joins the user-provided package argument directly onto apm_modules_path. This allows path traversal inputs (e.g. ../...) to escape apm_modules_path and can lead to reading arbitrary local files when display_package_info() loads metadata. Please validate the package path segments (and ensure_path_within) before constructing/using the path.

Copilot uses AI. Check for mistakes.
content = "\n".join(content_lines)
panel = Panel(
content,
title=f"[i] Package Info: {package}",
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Rich panel title uses "[i] Package Info: ...", but [i] is a Rich markup tag (italic), not a literal status symbol. This will render incorrectly (and may leak unclosed markup) on Rich-enabled terminals. Prefer emitting the literal [i] (escaped) or using STATUS_SYMBOLS['info']/CommandLogger output, and consider using the shared themed console (commands._helpers._get_console()) instead of creating a new Console() instance.

Suggested change
title=f"[i] Package Info: {package}",
title=f"[[i]] Package Info: {package}",

Copilot uses AI. Check for mistakes.
Comment on lines +238 to +252
try:
dep_ref = DependencyReference.parse(package)
except ValueError as exc:
_rich_error(f"Invalid package reference '{package}': {exc}")
sys.exit(1)

try:
downloader = GitHubPackageDownloader(auth_resolver=AuthResolver())
refs: List[RemoteRef] = downloader.list_remote_refs(dep_ref)
except RuntimeError as exc:
_rich_error(f"Failed to list versions for '{package}': {exc}")
sys.exit(1)

if not refs:
_rich_info(f"No versions found for '{package}'")
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

display_versions() takes a logger: CommandLogger argument but never uses it (it calls _rich_error/_rich_info directly). This makes the parameter misleading and can also trigger unused-arg linting. Either use the passed logger for reporting (consistent with other commands) or remove the parameter.

Suggested change
try:
dep_ref = DependencyReference.parse(package)
except ValueError as exc:
_rich_error(f"Invalid package reference '{package}': {exc}")
sys.exit(1)
try:
downloader = GitHubPackageDownloader(auth_resolver=AuthResolver())
refs: List[RemoteRef] = downloader.list_remote_refs(dep_ref)
except RuntimeError as exc:
_rich_error(f"Failed to list versions for '{package}': {exc}")
sys.exit(1)
if not refs:
_rich_info(f"No versions found for '{package}'")
def _report_error(message: str) -> None:
logger_error = getattr(logger, "error", None)
if callable(logger_error):
logger_error(message)
else:
_rich_error(message)
def _report_info(message: str) -> None:
logger_info = getattr(logger, "info", None)
if callable(logger_info):
logger_info(message)
else:
_rich_info(message)
try:
dep_ref = DependencyReference.parse(package)
except ValueError as exc:
_report_error(f"Invalid package reference '{package}': {exc}")
sys.exit(1)
try:
downloader = GitHubPackageDownloader(auth_resolver=AuthResolver())
refs: List[RemoteRef] = downloader.list_remote_refs(dep_ref)
except RuntimeError as exc:
_report_error(f"Failed to list versions for '{package}': {exc}")
sys.exit(1)
if not refs:
_report_info(f"No versions found for '{package}'")

Copilot uses AI. Check for mistakes.
Comment on lines +75 to +83
# Build a DependencyReference to query remote refs
try:
dep_ref = DependencyReference(
repo_url=dep.repo_url,
host=dep.host,
)
except Exception:
return (package_name, current_ref or "(none)", "-", "unknown", [])

Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_check_one_dep() reconstructs a DependencyReference from only repo_url and host. This drops host-specific fields (notably Azure DevOps ado_organization/project/repo), which GitHubPackageDownloader._build_repo_url() requires to form valid ADO clone URLs. As a result, apm outdated can mis-handle ADO dependencies (likely returning unknown or erroring). Consider constructing the reference via DependencyReference.parse(...) (including host prefix) or persisting the needed ADO fields in the lockfile and rehydrating them here.

Copilot uses AI. Check for mistakes.
Comment on lines +90 to +106
is_tag = _is_tag_ref(current_ref)

if is_tag:
tag_refs = [r for r in remote_refs if r.ref_type == GitReferenceType.TAG]
if not tag_refs:
return (package_name, current_ref, "-", "unknown", [])

latest_tag = tag_refs[0].name
current_ver = _strip_v(current_ref)
latest_ver = _strip_v(latest_tag)

if is_newer_version(current_ver, latest_ver):
extra = [r.name for r in tag_refs[:10]] if verbose else []
return (package_name, current_ref, latest_tag, "outdated", extra)
else:
return (package_name, current_ref, latest_tag, "up-to-date", [])
else:
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For tag-pinned deps, the code relies on is_newer_version() from utils/version_checker.py, which only parses versions like 1.2.3, 1.2.3a1, 1.2.3b1, 1.2.3rc1. Tags that are still semver-valid but common in Git (e.g. v1.2.3-beta, v1.2.3-rc.1) will be treated as invalid and therefore never reported as outdated (since is_newer_version() returns False on invalid input). If apm outdated is meant to do semver comparisons, consider using a semver-capable parser here or treating unparseable tags as unknown rather than up-to-date.

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +48
def _find_remote_tip(ref_name, remote_refs):
"""Find the tip SHA for a branch ref from remote refs.

If *ref_name* is empty/None, looks for HEAD or falls back to
common default branch names (main, master).
Returns the commit SHA string or None if not found.
"""
from ..models.dependency.types import GitReferenceType

if not remote_refs:
return None

branch_refs = {r.name: r.commit_sha for r in remote_refs
if r.ref_type == GitReferenceType.BRANCH}

if ref_name:
return branch_refs.get(ref_name)

# No ref specified -- find the default branch
# HEAD is included by git ls-remote; fall back to main/master
head_refs = [r for r in remote_refs if r.name == "HEAD"]
if head_refs:
return head_refs[0].commit_sha
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_find_remote_tip() claims it can look for HEAD in remote_refs, but list_remote_refs() calls git ls-remote --tags --heads and the parser only includes refs/heads/* and refs/tags/*, so a HEAD entry will never be present. This makes the default-branch logic/docstring misleading; either adjust list_remote_refs()/parser to include HEAD, or remove the HEAD branch and document the actual fallback behavior (main/master/first branch).

Copilot uses AI. Check for mistakes.
Sergio Sisternes and others added 2 commits April 8, 2026 10:32
- Add path validation in resolve_package_path() using path_security guards
- Fix ADO dependency handling in _check_one_dep() via DependencyReference.parse()
- Escape Rich markup symbols in panel titles ([[i]] not [i])
- Use logger param in display_versions() instead of direct _rich calls
- Remove dead HEAD check in _find_remote_tip()
- Clean up CHANGELOG entries with (microsoft#613) references

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Collaborator

@danielmeppiel danielmeppiel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make sure APM is familiar to devs, I'd name the "info" command "view", as in npm. See https://docs.npmjs.com/cli/v8/commands/npm-view

That's already the case for APM outdated (has equivalent in npm).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants